<?php
/**
 * Triple-B micro Framework for PHP5
 * Copyright (C) 2013  PT. Triple-B Shamany
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * -------------------------------------------------------------------
 * Berjalan diatas JVM memungkinkan integrasi PHP5 dengan sistem yang
 * lebih besar (SAP/JCo, Domino/Notes, etc) berbasis standar, dan
 * memanfaatkan connection-pooling (bahkan XA).
 *
 * Fitur micro Framework:
 *  ~ Berbasis REST (implementasi service-based design-pattern)
 *  ~ Super kecil, kurang dari 2000 line (plus comment)
 *  ~ Berbasis annotation (via comment) untuk menjamin (100%) integritas
 *    parameter yang diterima oleh script
 *  ~ Built-in JTA/XA dan connection pooling (via atomikos)
 *  ~ Built-in query/statement builder
 *  ~ Built-in GUI (implementasi declaration design-pattern)
 *  ~ and more..
 *
 * Untuk production, gunakan JRE x64 for Windows/Linux.
 *
 * Commercial Support: PT. Triple-B Shamany
 *           Services: Training, Development, Enterprise Integration
 *     Contact Person: Badrus Said <badrus.said@gmail.com>
 *
 * @author  Badrus Said <badrus.said@gmail.com>
 * @version 3.7-GA, 05/01/13
 */

/* java-impl: /servlet/context/ROOT/WEB-INF/classes/ */

/**
 * Seluruh implementasi Annotation adalah turunan dari interface
 * dibawah ini. Annotation akan diproses 1x pada saat pembuatan instance
 * pertama melalui fungsi getService()
 */
interface Annotation {
    /**
     * Kesempatan pertama yang akan diberikan kepada processor untuk
     * melakukan hook dan modifikasi bentuk raw atau object annotations
     * yang digunakan.
     *
     * @param   at  json raw-string
     * @param   rs  key-value pair annotation (object)
     *
     * @return  mixed boolean (success) or map
     */
    public function parse(/* String */ $at, /* stdClass */ $rs);

    /**
     * Implementasi annotations yang digunakan.
     *
     * @param   at  key-value pair annotation (parsed)
     * @param   rs  key-value pair annotation (object)
     */
    public function trace(/* String */ $at, /* stdClass */ $rs);
}

/* retention-policy: class/interface */
interface AnnotationClass extends Annotation {}

/* retention-policy: property */
interface AnnotationProperty extends Annotation {}

/* retention-policy: function/method */
interface AnnotationFunction extends Annotation {}

/**
 * Basis kelas (optional) annotations, bentuk konkrit dalam pengembangan
 */
class AnnotationProcessor implements Annotation
{
    /**
     * @see Annotation::parse(String $at, stdClass $rs)
     */
    public function parse(/* String */ $at, /* stdClass */ $rs) {return true;}

    /**
     * @see Annotation::trace(json $at, stdClass $rs)
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {return true;}
}

/**
 * Tujuannya adalah untuk memastikan bahwa request ini dipanggil oleh
 * client (apapun) dengan menyertakan header X-Requested-With: XMLHttpRequest.
 *
 * @usage   tambahkan annotation ajax untuk memastikan bahwa method
 *          yang dipanggil memiliki XMLHttpRequest.
 */
final class AnnotationProcessorAjax extends AnnotationProcessor
    implements AnnotationFunction
{
    /**
     * Hentikan proses jika request bukan XMLHttpRequest.
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {
        if (!getService()->isAjax()) quit(meta()->ajaxException);

        return true;
    }
}

/**
 * Memberikan kemudahan bagi developer untuk validasi data secara
 * "natural" melalui seleksi "character-type" lengkap dengan kriteria
 * yang diinginkan, melalui annotations
 */
class AnnotationProcessorArgument extends AnnotationProcessor
    implements AnnotationFunction
{
    private function _rfc($m) {
        $rx = meta()->regex;
        $sp = strrpos($m, "@");
        if ($sp === false) return false;
        $dp = substr($m, $sp + 1);
        $dL = strlen($dp);
        $ll = substr($m, 0, $sp);
        $lL = strlen($ll);
        $lR = str_replace($rx[9], STR_EMPTY, $ll);
        $rs = true;
        if ($lL < 1 || $lL > 64 || $dL < 1 || $dL > 255) {
            $rs = false;
        } elseif ($ll[0] == "." || $ll[$lL - 1] == ".") {
            $rs = false;
        } elseif (preg_match($rx[4], $ll) || !preg_match($rx[5], $dp) || preg_match($rx[6], $dp)) {
            $rs = false;
        } elseif (!preg_match($rx[7], $lR)) {
              if (!preg_match($rx[8], $lR)) $rs = false;
        }

        return $rs;
    }

    protected function validate(/* json */ $at, /* stdClass */ $rs, $n, $v) {
        $ch = meta();
        if (isset($at->type)) {
           switch($at->type) {
               case "alnum": if (!ctype_alnum($v)) quit($ch->alnum($n, $v)); break;
               case "alpha": if (!ctype_alpha($v)) quit($ch->alpha($n, $v)); break;
               case "digit": if (!ctype_digit($v)) quit($ch->digit($n, $v)); break;
               case "email": if (!$this->_rfc($v)) quit($ch->email($n, $v)); break;
               case "regex": if (!preg_match($at->regex, $v)) quit($ch->regexException($n, $at->regex, $v)); break;
               case "boolean": if ($v !== "true" || $v !== "false") quit($ch->boolean($n, $v)); break;
               case "date":
                   $d = getService(meta()->API->Date);
                   if (!((isset($at->dateFormat)) ? $d->check($v, $at->dateFormat) : $d->check($v)))
                   {
                       quit($ch->dateFormat($n, $v));
                   }
               break;
           }
        }
        $j = strlen($v);
        if (isset($at->maxl)) if ($j > $at->maxl) quit($ch->length($n, $v, $j));
        if (isset($at->minl)) if ($j < $at->minl) quit($ch->length($n, $v, $j));
        $x = (isset($at->minv)) ? true : false;
        $y = (isset($at->maxv)) ? true : false;
        if ($x || $y) {
            $z = floatval($v);
            if ($y) if ($z > floatval($at->maxv)) quit($ch->value($n, $v, ">", $at->maxv));
            if ($x) if ($z < floatval($at->minv)) quit($ch->value($n, $v, "<", $at->minv));
        }
    }

    /**
     * Khusus untuk tipe regular-expression, attribute regex harus disertakan!
     */
    public function parse(/* json */ $at, /* stdClass */ $rs) {
        $i = array();
        foreach($at as $j) {
            $rs = json_decode(trim($j));
            if (is_null($rs)) quit(meta()->annotationException);
            if (is_set($rs, "type", "regex")) if (!isset($rs->regex)) quit(meta()->annotationException);
            array_push($i, $rs);
        }

        return $i;
    }

    /**
     * Iterasi validasi untuk setiap parameter dengan identifikasi berdasarkan "name"
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {
        $v = getService()->arguments();
        $u = sizeof($at);
        $t = sizeof($v);

        if ($t < $u) quit(meta()->argumentException($u, $t));

        foreach($at as $k => $a) {
            $this->validate($a, $rs, $k, $v[$k]);
        }

        return true;
    }
}

/**
 * Pengecekan dari sisi server apakah client mendukung adanya cookie.
 */
final class AnnotationProcessorCookie extends AnnotationProcessor
    implements AnnotationFunction
{
    /**
     * Hentikan proses jika client tidak mendukung cookie. Hanya efektif jika
     * inisialisasi dilakukan pada "homepage"
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {
        if (getService()->supportCookie() == false) quit(meta()->cookieException);

        return true;
    }
}

/**
 * Notify framework bahwa output-buffer dikirim dalam format
 * terkompresi. Developer tidak perlu kuatir karena framework akan
 * menambahkan header yang dibutuhkan agar client mengerti "Content-Type/Encoding"
 * yang dikirim.
 */
final class AnnotationProcessorGzip extends AnnotationProcessor
    implements AnnotationFunction {}

/**
 * Disediakan bagi tipe developer yang "OOP maniac". Framework akan memetakan
 * parameter yang diterima kedalam public-property.
 *
 * Untuk pengembangan lebih lanjut, mengingat semua services dibentuk melalui
 * reflection, framework seharusnya bisa memetakan variabel ini kedalam property
 * protected atau bahkan private.
 */
final class AnnotationProcessorProperty extends AnnotationProcessorArgument
    implements AnnotationProperty
{
    /**
     * Attribute "expect" menunjukkan adanya ketergantungan presensi parameter
     * terhadap parameter yang lain dan jika hal ini tidak terpenuhi, maka
     * proses akan dihentikan.
     */
    public function parse(/* json */ $at, /* stdClass */ $rs) {
        $j = json_decode(array_shift($at));
        if (is_null($j)) quit(meta()->annotationException);
        if (!isset($j->name)) quit(meta()->propertyException);
        if (isset($j->expect)) if (!is_array($j->expect)) quit(meta()->expect);
        if (is_set($j, "type", "regex", true)) {
            if (!isset($j->regex)) quit(meta()->annotationException);
        }

        return $j;
    }

    /**
     * Proses pemetaan variabel request kedalam property service, framework juga
     * diharapkan menangani dependency sebuah variabel terhadap presensi variabel
     * lain bahkan terhadap metode request.
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {
        $i = $at->name;
        if (isset($at->expect)) {
            if (!in_array(strtoupper($_SERVER[meta()->env->mreq]), $at->expect)) return true;
        }
        if (!property_exists($rs, $i)) quit(meta()->noPropertyException(get_class($rs), $i));
        $j = getService()->getParameter($i);
        if (is_set($at, "required")) {
            if (is_null($j) || empty($j)) quit(meta()->requiredException($i));
        }
        if (!is_null($j)) $this->validate($at, $rs, $i, $j);

        return true;
    }
}

/**
 * Dependency injection.
 */
final class AnnotationProcessorInject extends AnnotationProcessor
    implements AnnotationProperty {}

/**
 * Seluruh object yang bisa dipanggil (content apapun) adalah sebuah resource
 * sesuai dengan konsep REST. Kebutuhan akan proteksi resource tertentu akan
 * muncul ketika konsep ini di-implementasikan untuk sebuah aplikasi.
 *
 * Kelas ini adalah abstraksi untuk resource yang tidak membutuhkan proteksi
 * atau ketergantungan terhadap informasi yang bersifat private.
 */
class AnnotationProcessorResource extends AnnotationProcessor
    implements AnnotationClass {}

/**
 * Kontrak pada resource ini adalah resource hanya bisa dipanggil secara
 * internal.
 */
class AnnotationProcessorProtected extends AnnotationProcessorResource {}

/**
 * Kontrak pada resource ini adalah resource hanya bisa dipanggil secara
 * internal oleh resource lain pada package yang sama.
 */
final class AnnotationProcessorPrivate extends AnnotationProcessorProtected {}

/**
 * Kontrak pada resource ini adalah pengguna harus melakukan login/teridentifikasi
 * sebagai pengguna dengan session aktif.
 */
final class AnnotationProcessorSecure extends AnnotationProcessorResource
{
    /**
     * Check apakah KEY identifikasi ada pada session pengguna. Jika rule tidak
     * terpenuhi, arahkan ke halaman login.
     */
    public function trace(/* json */ $at, /* stdClass */ $rs) {
        if (!isset($_SESSION[meta()->Session->id])) {
            getService(meta()->API->Response)->redirect(FILE_SEPARATOR);
        }

        return true;
    }
}

/**
 * doclet
 */
final class AnnotationProcessorAuthor extends AnnotationProcessorResource {}

/**
 * doclet
 */
final class AnnotationProcessorVersion extends AnnotationProcessorResource {}

/**
 * Informasi resource kepada container bahwa content-delivery akan ditangani
 * secara independen.
 */
final class AnnotationProcessorSilent extends AnnotationProcessor
    implements AnnotationFunction {}

/**
 * Container akan menjamin bahwa instance resource akan diciptakan 1x
 * pada saat lookup pertama dan digunakan sepanjang eksekusi.
 */
final class AnnotationProcessorSingleton extends AnnotationProcessor
    implements AnnotationClass {}

/**
 * Untuk memudahkan serta tujuan konsistensi OOP, beberapa fungsi2 native
 * di-enkapsulasi kedalam wrapper.
 */
abstract class Wrapper {
    private $resource = null, $prefix = null, $lookup = null;
    protected function setPrefix($prefix) {
        $this->prefix = $prefix;
    }
    protected function setLookup(array $lookup) {
        $this->lookup = $lookup;
    }
    protected function setResource($resource) {
        $this->resource = $resource;
    }

    /**
     * @return  native resource
     */
    public function getResource() {return $this->resource;}

    /**
     * magic method to call native function.
     *
     * @return  "AS IS" native function output
     */
    public function __call($name, array $arguments) {
        if (!is_null($this->resource)) array_unshift($arguments, $this->resource);
        if (!is_null($this->lookup)) if (isset($this->lookup[$name])) $name = $this->lookup[$name];
        if (!is_null($this->prefix)) {
            $f = $this->prefix.$name;
            if (!function_exists($f)) $f = $this->prefix."_".$name; /* yes, they did it! */
            $name = $f;
        }

        return call_user_func_array($name, $arguments);
    }
}

/**
 * Bertindak sebagai "Template engine" sekaligus buffer content yang akan
 * dikirim sebagai response.
 *
 * @singleton
 * @private
 */
final class OutputStream {
    private $stack = array();
    public function __destruct() {$this->stack = null;}
    public function __construct() {
        Framework::register($this);
        ob_implicit_flush(0);
    }

    /**
     * Penggunaan array sebagai stack untuk menjamin bahwa proses rendering
     * template sesuai urutan eksekusi, pengguna diberikan kebebasan untuk
     * mengambil raw-result (bypass-stack) jika diperlukan.
     */
    public function load($fileName, array $arguments = null, $fetch = false) {
        if (!is_null($arguments)) extract($arguments);

        require $fileName;

        $buffer = ob_get_clean();
        if ($fetch) {
            $fetch = $buffer;
        } else {
            array_push($this->stack, $buffer);
            $fetch = true;
        }

        return $fetch;
    }

    /**
     * Beberapa kasus pengembangan membutuhkan injeksi pada stack secara langsung
     * untuk kebutuhan debugging, etc.
     */
    public function push() {
        $j = func_num_args();
        for($i = 0; $i < $j; $i++) {
            $k = func_get_arg($i);
            array_push($this->stack,
                (is_array($k) || is_object($k)) ? print_r($k, true) : ((is_resource($k)) ? get_resource_type($k) : $k)
            );
        }
    }

    /**
     * Output keseluruhan content pada stack.
     */
    public function flush() {
        $tmpl = null;
        if (sizeof($this->stack) > 0) {
            $tmpl = implode(STR_EMPTY, $this->stack);
            $this->stack = array();
        }
        return $tmpl;
    }
}

/**
 * Asumsinya adalah pada saat lookup, koneksi akan dibuat (digunakan atau tidak)
 * dan pada akhir eksekusi script akan ditutup secara otomatis untuk
 * mengurangi penulisan kode yang tidak perlu: connect dan close.
 */
final class Connection {
    const ORACLE = 0, SYBASE = 1, MSSQL = 2, DB2 = 3, POSTGRESQL = 4, MYSQL = 5;

    private $jdbc, $cursor = 0;

    /**
     * Untuk alasan konsistensi, developer dibebaskan dari keharusan menutup
     * koneksi yang terbuka. Seluruh resource akan ditutup pada akhir eksekusi script
     * via destructor.
     */
    public function __destruct() {$this->jdbc->close();}
    public function __construct($jndi) {
        $this->jdbc = new Java("ufwk.JDBC", sprintf("java:comp/env/jdbc/%s", $jndi));
    }

    /**
     * @return  database used protocol name
     */
    public function getProductName() {
        return $this->jdbc->getProductName();
    }

    /**
     * BEGIN TRANSACTION
     */
    public function begin() {
        if ($this->cursor == 0) $this->jdbc->beginTransaction();
        $this->cursor++;
    }

    /**
     * Makes all changes made since the previous commit/rollback permanent and
     * releases any database locks currently held by this <code>Connection</code>
     * object.
     * This method should be used only when auto-commit mode has been disabled.
     */
    public function commit() {
        if ($this->cursor > 0) {
            if ($this->cursor == 1) $this->jdbc->commit();
            $this->cursor--;
        }
    }

    /**
     * Undoes all changes made in the current transaction and releases any
     * database locks currently held by this <code>Connection</code> object.
     * This method should be  used only when auto-commit mode has been disabled.
     */
    public function rollback() {
        if ($this->cursor > 0) {
            if ($this->cursor == 1) $this->jdbc->rollback();
            $this->cursor--;
        }
    }

    /**
     * <P>Creates a <code>Statement</code> object for sending SQL statements to the
     * database. SQL statements without parameters are normally
     * executed using <code>Statement</code> objects. If the same SQL statement
     * is executed many times, it may be more efficient to use a
     * <code>PreparedStatement</code> object.</P>
     *
     * <P>
     * Result sets created using the returned <code>Statement</code>
     * object will by default be type <code>TYPE_FORWARD_ONLY</code>
     * and have a concurrency level of <code>CONCUR_READ_ONLY</code>.
     * The holdability of the created result sets can be determined by
     * calling {@link #getHoldability}.
     *
     * @return a new <code>Statement</code> object
     */
    public function createStatement($namespace = null) {
        return new Statement($this, $namespace);
    }

    /**
     * Escape string
     */
    public function escape($text) {
        return $this->jdbc->escape($text);
    }

    /**
     * Executes the given SQL statement, which returns a single
     * <code>ResultSet</code> object.
     *
     * @param sql an SQL statement to be sent to the database, typically a
     *        static SQL <code>SELECT</code> statement
     * @return a <code>ResultSet</code> object that contains the data produced
     *         by the given query; never <code>null</code>
     */
    public function executeQuery() {
        $j = func_num_args();
        $q = func_get_arg(0);
        if ($j > 1) {
            for($i = 1, $v = null; $i < $j; $i++) $v[] = $this->escape(func_get_arg($i));
            $q = vsprintf($q, $v);
        }

        return new ResultSet($this, $this->jdbc->executeQuery($q));
    }

    /**
     * Executes the given SQL statement, which may be an <code>INSERT</code>,
     * <code>UPDATE</code>, or <code>DELETE</code> statement or an
     * SQL statement that returns nothing, such as an SQL DDL statement.
     *
     * @param sql an SQL Data Manipulation Language (DML) statement, such as <code>INSERT</code>, <code>UPDATE</code> or
     * <code>DELETE</code>; or an SQL statement that returns nothing,
     * such as a DDL statement.
     *
     * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
     *         or (2) 0 for SQL statements that return nothing
     */
    public function executeUpdate() {
        $j = func_num_args();
        $q = func_get_arg(0);
        if ($j > 1) {
            for($i = 1, $v = null; $i < $j; $i++) $v[] = $this->escape(func_get_arg($i));
            $q = vsprintf($q, $v);
        }

        return $this->jdbc->executeUpdate($q);
    }

    /**
     * Retrieves any auto-generated keys created as a result of executing this
     * <code>Statement</code> object. If this <code>Statement</code> object did
     * not generate any keys, an empty <code>ResultSet</code>
     * object is returned.
     *
     * <p><B>Note:</B>If the columns which represent the auto-generated keys were not specified,
     * the JDBC driver implementation will determine the columns which best represent the auto-generated keys.
     *
     * @return  last auto-generated key(s) generated by the execution of this <code>Connection</code> object
     */
    public function lastInsertId() {
        $this->jdbc->lastInsertId();
    }
}

/**
 * A table of data representing a database result set, which
 * is usually generated by executing a statement that queries the database.
 *
 * <P>A <code>ResultSet</code> object  maintains a cursor pointing
 * to its current row of data.  Initially the cursor is positioned
 * before the first row. The <code>next</code> method moves the
 * cursor to the next row, and because it returns <code>false</code>
 * when there are no more rows in the <code>ResultSet</code> object,
 * it can be used in a <code>while</code> loop to iterate through
 * the result set.
 * <P>
 *
 * A default <code>ResultSet</code> object is not updatable and
 * has a cursor that moves forward only.  Thus, you can
 * iterate through it only once and only from the first row to the
 * last row.
 * <PRE>
 *       $rset = $conn->executeQuery("SELECT A, B FROM TABLE_NAME");
 * </PRE>
 *
 * The <code>ResultSet</code> interface provides
 * <i>getter</i> methods (<code>getInt</code>, <code>getFloat</code>, and so on)
 * for retrieving column values from the current row.
 *
 * <P>Column names used as input to getter methods are case
 * insensitive.  When a getter method is called  with
 * a column name and several columns have the same name,
 * the value of the first matching column will be returned.
 * The column name option is
 * designed to be used when column names are used in the SQL
 * query that generated the result set.
 * For columns that are NOT explicitly named in the query, it
 * is best to use column numbers. If column names are used, the
 * programmer should take care to guarantee that they uniquely refer to
 * the intended columns, which can be assured with the SQL <i>AS</i> clause.
 * <P>
 */
final class ResultSet implements Iterator
{
    private $conn, $rset;

    public function __destruct() {
        $this->rset = null;
        $this->conn = null;
    }
    public function __construct(Connection $conn, $rset) {
        $this->conn = $conn;
        $this->rset = $rset;
    }

    /**
     * Untuk memudahkan fetching kolom pada saat iterasi, programmer bisa memanfaatkan
     * overloading method field sebagai property.
     */
    public function __get($a) {return $this->get($a);}

    /**
     * row-number
     */
    public function getRow() {return $this->rset->getRow();}

    /**
     * Moves the cursor to the previous row in this
     * <code>ResultSet</code> object.
     * <p>
     * When a call to the <code>previous</code> method returns <code>false</code>,
     * the cursor is positioned before the first row.  Any invocation of a
     * <code>ResultSet</code> method which requires a current row will result in a
     * <code>SQLException</code> being thrown.
     * <p>
     * If an input stream is open for the current row, a call to the method
     * <code>previous</code> will implicitly close it.  A <code>ResultSet</code>
     *  object's warning change is cleared when a new row is read.
     * <p>
     *
     * @return <code>true</code> if the cursor is now positioned on a valid row;
     * <code>false</code> if the cursor is positioned before the first row
     */
    public function previous() {return $this->rset->previous();}

    /**
     * Moves the cursor a relative number of rows, either positive or negative.
     * Attempting to move beyond the first/last row in the
     * result set positions the cursor before/after the
     * the first/last row. Calling <code>relative(0)</code> is valid, but does
     * not change the cursor position.
     *
     * <p>Note: Calling the method <code>relative(1)</code>
     * is identical to calling the method <code>next()</code> and
     * calling the method <code>relative(-1)</code> is identical
     * to calling the method <code>previous()</code>.
     *
     * @param rows an <code>int</code> specifying the number of rows to
     *        move from the current row; a positive number moves the cursor
     *        forward; a negative number moves the cursor backward
     * @return <code>true</code> if the cursor is on a row;
     *         <code>false</code> otherwise
     */
    public function relative($cursor) {return $this->rset->relative($cursor);}

    /**
     * Moves the cursor to the given row number in
     * this <code>ResultSet</code> object.
     *
     * <p>If the row number is positive, the cursor moves to
     * the given row number with respect to the
     * beginning of the result set.  The first row is row 1, the second
     * is row 2, and so on.
     *
     * <p>If the given row number is negative, the cursor moves to
     * an absolute row position with respect to
     * the end of the result set.  For example, calling the method
     * <code>absolute(-1)</code> positions the
     * cursor on the last row; calling the method <code>absolute(-2)</code>
     * moves the cursor to the next-to-last row, and so on.
     *
     * <p>An attempt to position the cursor beyond the first/last row in
     * the result set leaves the cursor before the first row or after
     * the last row.
     *
     * <p><B>Note:</B> Calling <code>absolute(1)</code> is the same
     * as calling <code>first()</code>. Calling <code>absolute(-1)</code>
     * is the same as calling <code>last()</code>.
     *
     * @param row the number of the row to which the cursor should move.
     *        A positive number indicates the row number counting from the
     *        beginning of the result set; a negative number indicates the
     *        row number counting from the end of the result set
     * @return <code>true</code> if the cursor is moved to a position in this
     * <code>ResultSet</code> object;
     * <code>false</code> if the cursor is before the first row or after the
     * last row
     */
    public function absolute($cursor) {return $this->rset->absolute($cursor);}

    /**
     * Moves the cursor to the first row in
     * this <code>ResultSet</code> object.
     *
     * @return <code>true</code> if the cursor is on a valid row;
     * <code>false</code> if there are no rows in the result set
     * @exception SQLException if a database access error
     * occurs; this method is called on a closed result set
     * or the result set type is <code>TYPE_FORWARD_ONLY</code>
     */
    public function first() {return $this->rset->first();}

    /**
     * Moves the cursor to the last row in
     * this <code>ResultSet</code> object.
     *
     * @return <code>true</code> if the cursor is on a valid row;
     * <code>false</code> if there are no rows in the result set
     * @exception SQLException if a database access error
     * occurs; this method is called on a closed result set
     * or the result set type is <code>TYPE_FORWARD_ONLY</code>
     */
    public function last() {return $this->rset->last();}

    /**
     * Moves the cursor to the front of
     * this <code>ResultSet</code> object, just before the
     * first row. This method has no effect if the result set contains no rows.
     *
     * @exception SQLException if a database access error
     * occurs; this method is called on a closed result set or the
     * result set type is <code>TYPE_FORWARD_ONLY</code>
     */
    public function beforeFirst() {$this->rset->beforeFirst();}

    /**
     * Moves the cursor to the end of
     * this <code>ResultSet</code> object, just after the
     * last row. This method has no effect if the result set contains no rows.
     * @exception SQLException if a database access error
     * occurs; this method is called on a closed result set
     * or the result set type is <code>TYPE_FORWARD_ONLY</code>
     */
    public function afterLast() {$this->rset->afterLast();}

    /**
     * Retrieves the value of the designated column in the current row
     * of this <code>ResultSet</code> object as
     * an <code>int</code> in the Java programming language.
     *
     * @param columnIndex the first column is 1, the second is 2, ...
     * @return the column value; if the value is SQL <code>NULL</code>, the
     * value returned is <code>0</code>
     */
    public function getInt($column) {return intval($this->get($column));}

    /**
     * Retrieves the value of the designated column in the current row
     * of this <code>ResultSet</code> object as
     * a <code>float</code> in the Java programming language.
     *
     * @param columnIndex the first column is 1, the second is 2, ...
     * @return the column value; if the value is SQL <code>NULL</code>, the
     * value returned is <code>0</code>
     */
    public function getFloat($column) {return floatval($this->get($column));}

    /**
     * <p>Gets the value of the designated column in the current row
     * of this <code>ResultSet</code> object as
     * an <code>Object</code> in the Java programming language.
     *
     * <p>This method will return the value of the given column as a
     * Java object.  The type of the Java object will be the default
     * Java object type corresponding to the column's SQL type,
     * following the mapping for built-in types specified in the JDBC
     * specification. If the value is an SQL <code>NULL</code>,
     * the driver returns a Java <code>null</code>.
     *
     * <p>This method may also be used to read database-specific
     * abstract data types.
     */
    public function get($column, $default = null) {
        $value = $this->rset->getString((is_int($column) ? $column : $column));
        if (is_null($value)) {
            if (!is_null($default)) $value = $default;
        }

        return $value;
    }

    /**
     * Forward pointer.
     */
    public function next() {return $this->rset->next();}

    /**
     * Check connection.
     */
    public function valid() {return $this->rset->isAfterLast();}

    /**
     * Kembalikan pointer pada posisi awal.
     */
    public function rewind() {if (!$this->beforeFirst()) $this->absolute(0);}

    /**
     * Ambil posisi pointer.
     */
    public function key() {}

    /**
     * Fields pada posisi pointer saat ini.
     */
    public function current() {}
}

/**
 * TextBuilder digunakan untuk memudahkan pembentukan statement (on-the-fly)
 * berdasarkan informasi/parameter yang ada.
 */
final class StatementTextBuilder {
    private $stmt;

    public function __construct($stmt = STR_EMPTY) {$this->stmt = $stmt;}
    public function __destruct() {$this->stmt = null;}

    /**
     * Check apakah text kosong
     */
    public function notEmpty() {return !empty($this->stmt);}

    /**
     * Tambahkan text
     */
    public function append($stmt) {$this->stmt.= $stmt;return $this;}

    /**
     * Hasil akhir text
     */
    public function toString() {return $this->stmt;}
}

/**
 * Statement adalah wrapper dari statement SQL (CREATE/UPDATE/DELETE) dalam bentuk
 * OOP untuk memudahkan logic pembentukan statement. Sangat efektif digunakan
 * untuk membentuk statement select pada saat reporting dimana statement yang
 * dibentuk sangat bergantung (berubah) dari parameter yang dikirim.
 */
final class Statement {
    const EQ = 0, NOT_EQ = 1, GT = 2, LT = 3, GTE = 4, LTE = 5, LIKE = 6, NOT_LIKE = 7, ILIKE = 8, NOT_ILIKE = 9, JOIN = 10, IN = 11, NOT_IN = 12;
    private $database = array(
        "oracle", "sybase", "mssql", "db2", "postgresql", "mysql"
    );
    private $join = array(" AND ", " OR ");
    private $oper = array(
        "=", "<>", ">", "<", ">=", "<=", " LIKE ", " NOT LIKE ", " ILIKE ", " NOT ILIKE ", " JOIN ", " IN ", " NOT IN "
    );
    private $sqlt = (object) array(
        "ob" => "(",
        "cb" => ")",
        "sp" => ", ",
        "sq" => "', '",
        "sc" => " ",
        "io" => " (",
        "ic" => ") Q_TABLE",
        "it" => "INSERT INTO ",
        "up" => "UPDATE ",
        "sl" => "SELECT ",
        "dl" => "DELETE FROM ",
        "fr" => " FROM ",
        "st" => " SET ",
        "wh" => " WHERE ",
        "dc" => ";",
        "gb" => " GROUP BY ",
        "hv" => " HAVING ",
        "od" => " ORDER BY ",
        "l1" => "SELECT B.* FROM (SELECT A.*, ",
        "l2" => "rownum",
        "l3" => "row_number() over()",
        "l4" => ") A) B WHERE ",
        "l5" => "B.SQL_ROWNUM > ",
        "l6" => " AND B.SQL_ROWNUM <= ",
        "l7" => " B.SQL_ROWNUM <= ",
        "l8" => " AS SQL_ROWNUM FROM (",
        "li" => " LIMIT ",
        "of" => " OFFSET ",
        "in" => " ('%s') ",
        "sv" => "'%s'"
    );
    private $conn, $type = null, $from = null, $args;
    private $limit = 0, $offset = 0, $stmt = null, $cols = null, $jkey = -1;
    private $groupBy = null, $orderBy = null;
    private $L1 = null, $L2 = null;

    private $columns = null;

    public function __destruct() {
        $this->conn = null;
    }
    public function __construct($conn, $from = null) {
        $this->conn = $conn;
        $this->type = $conn->getProductName();
        $this->reset();
        if (!is_null($from)) {
            $this->from = $from;
            if (CACHE) {
                //
                // load columns from APC, used for auto-mapping CRUD
                //
            }
        }
    }

    /**
     * Kembalikan ke posisi awal query, efektif digunakan jika query dilakukan
     * dalam jumlah besar untuk menghindari alokasi memory yang berlebihan.
     */
    public function reset() {
        $this->limit = $this->offset = 0;
        $this->stmt = new StatementTextBuilder();
        $this->args = array();
        $this->cols = array();
        $this->jkey = -1;
        $this->groupBy = $this->orderBy = null;
        $this->L1 = $this->L2 = STR_EMPTY;
    }

    /**
     * Auto mapping berdasarkan parameter atau given map
     */
    public function autoMap(array $from = null) {
        $ufwk = getService();
        $auto = (is_null($from)) ? true : false;
        for($i = 0, $j = sizeof($this->columns); $i < $j; $i++) {
            $name = $this->columns[$i];
            $argv = ($auto) ? $ufwk->getParameter($name) : ((isset($from[$name])) ? $from[$name] : null);
            if (!is_null($argv)) {
                $this->set($name, $argv);
            }
        }
    }

    /**
     * Mapping target pada entity/tabel. Pada kasus operasi SELECT, bisa
     * lebih dari satu.
     */
    public function from() {
        $fn = func_num_args();
        $fr = array();
        $sp = false;
        for($i = 0; $i < $fn; $i++) {
            if ($sp) array_push($fr, $this->sqlt->sp);
            array_push($fr, func_get_arg($i));
            if (!$sp) $sp = true;
        }
        $this->from = implode(STR_EMPTY, $fr);

        return $this;
    }

    /**
     * Menentukan limit dan offset (pada operasi SELECT). Kelas secara
     * otomatis akan membentuk statement berdasarkan tipe database yang digunakan.
     */
    public function limit($limit = 0, $offset = 0) {
        $this->limit = $limit;
        $this->offset = $offset;

        return $this;
    }

    /**
     * "(" open bracket (tanda kurung buka)
     */
    public function ob() {
        $this->stmt->append($this->sqlt->ob);

        return $this;
    }

    /**
     * ")" close bracket (tanda kurung tutup)
     */
    public function cb() {
        $this->stmt->append($this->sqlt->cb);

        return $this;
    }

    /**
     * join operator: AND
     */
    public function and() {
        $this->stmt->append($this->join[0]);
        $this->jkey = -1;

        return $this;
    }

    /**
     * join operator: OR
     */
    public function or() {
        $this->stmt->append($this->join[1]);
        $this->jkey = -1;

        return $this;
    }

    /**
     * grouping query, bisa lebih dari satu kolom
     */
    public function groupBy() {$j = func_num_args();
        $this->groupBy = array();
        for($i = 0; $i < $j; $i++) $this->groupBy[$i] = func_get_arg($i);

        return $this;
    }

    /**
     * sorting query, bisa lebih dari satu kolom
     */
    public function orderBy() {$j = func_num_args();
        $this->orderBy = array();
        for($i = 0; $i < $j; $i++) $this->orderBy[$i] = func_get_arg($i);

        return $this;
    }

    /**
     * overloading method untuk setting nilai kolom: default equal join
     */
    public function __set($column, $value) {$this->set($column, $value);}

    /**
     * gunakan jika join yang akan digunakan selain "equal"
     */
    public function set($column, $value, $join = Statement::EQ) {
        static $sequence = 0;
        if ($this->jkey >= 0) $this->stmt->append($this->join[$this->jkey]);
        if ($value instanceof Statement) {
            if ($join == self::IN || $join == self::NOT_IN) {
                $this->stmt->append($column)->append($this->sqlt->sc)->append($this->oper[$join])->append($this->sqlt->io);
                $this->stmt->append($value->selectStatement())->append($this->sqlt->ic);
                $this->stmt->append($sequence++);
            }
        } else {
            if (is_array($value)) {
                foreach($value as $x => $z) $value[$x] = $this->conn->escape($z);
                $this->stmt->append($column)->append($this->oper[self::IN])->append($this->sqlt->in(implode($this->sqlt->sq, $value)));
            } else {
                $t = sprintf($this->sqlt->sv, $this->conn->escape($value));
                $this->stmt->append($column)->append($this->oper[$join])->append($t);
                $this->args[$column] = $t;
            }
        }
        array_push($this->cols, $column);
        $this->jkey = 0;

        return $this;
    }

    /**
     * eksekusi parameter statement sebagai operasi INSERT
     */
    public function insert() {
        return $this->conn->executeUpdate($this->insertStatement(), true);
    }

    /**
     * ambil SQL statement INSERT yang dibentuk dari parameter
     */
    public function insertStatement() {
        $stmt = new StatementTextBuilder();
        $stmt->append($this->sqlt->it)->append($this->from)->append($this->sqlt->st);
        $s = false;
        foreach($this->args as $k => $v) {
            if ($s) $stmt->append($this->sqlt->sp);
            $stmt->append($k)->append($this->oper[self::EQ])->append($v);
            if (!$s) $s = true;
        }

        return $stmt->toString();
    }

    /**
     * eksekusi parameter statement sebagai operasi UPDATE
     */
    public function update(array $keyPair) {
        return $this->conn->executeUpdate($this->updateStatement($keyPair));
    }

    /**
     * ambil SQL statement UPDATE yang dibentuk dari parameter dan array argument
     */
    public function updateStatement(array $keyPair) {
        $stmt = new StatementTextBuilder();
        $stmt->append($this->sqlt->up)->append($this->from)->append($this->sqlt->st);
        $s = false;
        foreach($keyPair as $k => $v) {
            if ($s) $stmt->append($this->sqlt->sp);
            $stmt->append($k)->append($this->oper[self::EQ])->append($this->conn->escape($v));
            if (!$s) $s = true;
        }
        if ($this->stmt->notEmpty()) {
            $stmt->append($this->sqlt->wh)->append($this->stmt->toString());
        }

        return $stmt->append($this->sqlt->dc)->toString();
    }

    /**
     * eksekusi parameter statement sebagai operasi DELETE
     */
    public function delete() {
        return $this->conn->executeUpdate($this->deleteStatement());
    }

    /**
     * ambil SQL statement DELETE yang dibentuk dari parameter
     */
    public function deleteStatement() {
        $stmt = new StatementTextBuilder();
        $stmt->append($this->sqlt->dl)->append($this->from);
        if ($this->stmt->notEmpty()) {
            $stmt->append($this->sqlt->wh)->append($this->stmt->toString());
        }

        return $stmt->append($this->sqlt->dc)->toString();
    }

    /**
     * eksekusi parameter statement sebagai operasi SELECT
     */
    public function select() {
        return $this->conn->executeQuery($this->createSelectStatement(func_get_args()));
    }

    /**
     * ambil SQL statement UPDATE yang dibentuk dari parameter dan arguments
     */
    public function selectStatement() {
        return $this->createSelectStatement(func_get_args());
    }

    private function createSelectStatement(array $args) {
        $stmt = new StatementTextBuilder();
        $stmt->append($this->sqlt->sl);
        $size = sizeof($args);
        if ($size > 0) {
            for($i = 0, $k = false; $i < $size; $i++) {
                if ($k) $stmt->append($this->sqlt->sp);
                $stmt->append($args[$i]);
                if (!$k) $k = true;
            }
        } else {
            $cols = implode($this->sqlt->sp, $this->cols);
            $stmt->append(($cols == STR_EMPTY) ? "*" : $cols);
        }
        $stmt->append($this->sqlt->fr)->append($this->from);
        if ($this->stmt->notEmpty()) $stmt->append($this->sqlt->wh)->append($this->stmt->toString());
        if ($this->groupBy != null) $this->at($t, $this->sqlt->gb, $this->groupBy);
        if ($this->orderBy != null) $this->at($t, $this->sqlt->od, $this->orderBy);
        if ($this->limit > 0) {
            if ($this->type == $this->database[self::ORACLE] || $this->type == $this->database[self::SYBASE]) {
                $this->L1 = $this->sqlt->l1.(($this->type == $this->database[self::ORACLE]) ? $this->sqlt->l2 : $this->sqlt->l3).$this->sqlt->l8;
                $this->L2 = $this->sqlt->l4;
                if ($this->offset > 0) {
                    $this->L2.= $this->sqlt->l5.$this->offset;
                    if ($this->limit > 0) $this->L2.= $this->sqlt->l6.($this->offset + $this->limit);
                } else {
                    $this->L2.= $this->sqlt->l7.$this->limit;
                }
            } else if ($this->type == $this->database[self::POSTGRESQL]) {
                $stmt->append($this->sqlt->li)->append($this->limit);
                if ($this->offset > 0) $stmt->append($this->sqlt->of)->append($this->offset);
            } else {
                $stmt->append($this->sqlt->li);
                if ($this->offset > 0) {
                    $stmt->append($this->offset)->append($this->sqlt->sp)->append($this->limit);
                } else {
                    if ($this->limit > 0) $stmt->append($this->limit);
                }
            }
        }
        $r = new StatementTextBuilder();
        $r->append($this->L1)->append($stmt->toString())->append($this->L2)->append($this->sqlt->dc);

        return $r->toString();
    }
    private function at($q, $t, array $u) {
        $q->append($t);
        for($i = 0, $j = sizeof($u), $s = false; $i < $j; $i++) {
            if ($s) $q->append($this->sqlt->sp);
            $q->append($u[$i]);
            if (!$s) $s = true;
        }
    }
}

/**
 * Kelas ini dibuat sebagai "resource bundle" mengingat beberapa string
 * yang digunakan untuk kebutuhan berbeda memiliki kesamaan.
 *
 * @singleton
 * @private
 */
final class MetaHelper {
    private $json;
    public function __destruct() {$this->json = null;}
    public function __construct($file = "Framework") {
        $post = strrpos($file, FILE_SEPARATOR);
        $name = ($post === false) ? sprintf("META-INF/%s.php", $file) : sprintf("%s/META-INF/%s.php", substr($file, 0, $post), substr($file, $post + 1));
        $flag = false;
        if (CACHE) $this->json = apc_fetch($name, $flag);
        if ($flag == false) {
            $json = include $name;
            if (CACHE) apc_store($name, $json);
            $this->json = $json;
        }
    }

    public function contains($name) {return (isset($this->json[$name]));}
    public function getMap() {return $this->json;}

    /**
     * Magic method: memanggil index sebagai fungsi (dengan arguments)
     *
     * @return  formatted text
     */
    public function __call($name, $arguments) {
        return (isset($this->json[$name])) ? vsprintf($this->json[$name], $arguments) : $name;
    }

    /**
     * Magic method: memanggil index sebagai property
     *
     * @return  Text instance (if not exist) or value
     */
    public function __get($name) {
        return (isset($this->json[$name])) ? $this->json[$name] : ($this->json[$name] = new MetaHelper(strtr($name, "_", FILE_SEPARATOR)));
    }
}

/**
 * Quercus membuat dua buah cookie untuk menangani session (servlet dan default
 * yang akan di-expose dilingkungan script). Untuk alasan portabilitas, framework
 * menggunakan default session (kemungkinan integrasi dengan G-WAN untuk versi
 * selanjutnya).
 *
 * @singleton
 * @private
 */
final class SessionHelper {
    private $addr = null;
    public function __destruct() {session_write_close();}
    public function __construct() {
        Framework::register($this);
        $text = meta()->env;
        $this->addr = (isset($_SERVER[$text->hcli])) ? $_SERVER[$text->hcli] : ((isset($_SERVER[$text->hxfo])) ? $_SERVER[$text->hxfo] : ((isset($_SERVER[$text->hxff])) ? $_SERVER[$text->hxff] : ((isset($_SERVER[$text->addr])) ? $_SERVER[$text->addr] : "127.0.0.1")));
        session_set_save_handler(
            array($this, "op"), array($this, "cl"), array($this, "rd"), array($this, "wr"), array($this, "ds"), array($this, "gc")
        );
        session_start();
    }

    /**
     * OPEN
     */
    public function op($path, $name) {return true;}

    /**
     * CLOSE
     */
    public function cl() {return true;}

    /**
     * READ
     */
    public function rd($i) {
        $j = STR_EMPTY;
        $c = getConnection();
        $k = $c->executeQuery(meta()->Session->rd, $i, time() - 2700);
        if ($k->next()) {
            $j = $k->SSN_DATA;
            $c->executeUpdate(meta()->Session->up, time(), $i);
        }

        return $j;
    }

    /**
     * WRITE
     */
    public function wr($i, $j) {$r = false;
        try {
            $c = getConnection();
            if (isset($_SESSION[meta()->Session->id])) {
                $c->executeUpdate(meta()->Session->wr, $i, $_SESSION[meta()->Session->id], $this->addr, $j, time());
            } else {
                $c->executeUpdate(meta()->Session->wg, $i, $this->addr, $j, time());
            }
            $r = true;
        } catch(Exception $e) {
            quit($e->getMessage());
        }

        return $r;
    }

    /**
     * DESTROY
     */
    public function ds($i) {getConnection()->executeUpdate(meta()->Session->ds, $i);}

    /**
     * GARBAGE COLLECTOR
     */
    public function gc($i) {
        $conn = getConnection();
        $time = time() - intval($i);
        $conn->executeUpdate(meta()->Session->gr, $time);
        $conn->executeUpdate(meta()->Session->gc, $time);
    }
}

/**
 * @singleton
 * @private
 */
final class Response {
    private $headers = array();
    private $content = null;
    private $code = 200;
    private $expired = 31104000;

    private function setCache($ts) {
        $headers = array(
            sprintf("Expires: %s GMT", gmdate("D, d M Y H:i:s", $ts + $this->expired)),
            sprintf("Last-Modified: %s GMT", gmdate("D, d M Y H:i:s", $ts)),
            sprintf("Cache-Control: max-age=%d, must-revalidate", $this->expired)
        );
        foreach($headers as $header) $this->setHeader($header);
    }

    /**
     * Set HTTP response code
     *
     * @void
     */
    public function setCode($code) {$this->code = $code;}

    /**
     * Get HTTP response code
     *
     * @return  int http code
     */
    public function getCode() {return $this->code;}

    /**
     * Set HTTP response header
     *
     * @void
     */
    public function setHeader($header) {
        if (GWAN) {
            array_push($this->headers, $header);
        } else {
            header($header, true);
        }
    }

    /**
     * Set HTTP content body
     *
     * @void
     */
    public function setContent($content) {$this->content = $content;}

    /**
     * Send HTTP response redirect
     *
     * @void
     */
    public function redirect($location) {
        $ajax = getService()->isAjax();
        $this->setHeader(sprintf("%s: %s", ($ajax) ? "Redirect" : "Location", $location));
        $this->setCode(($ajax) ? 200 : 302);
        $this->flush();
    }

    /**
     * Flush available contents
     *
     * @void
     */
    public function flush() {
        $output = "";
        if (sizeof($this->headers) > 0) {
            $output.= implode("\r\n", $this->headers);
            $output.= "\r\n";
        }
        if (!is_null($this->content)) $output.= $this->content;
        if (!empty($output)) {
            file_put_contents(STDOUT, $output);
        }
        exit($this->getCode());
    }

    /**
     * Send HTTP response cache headers
     *
     * @void
     */
    public function sendNotModified($ts) {
        $this->setCache($ts);
        $this->setHeader("HTTP/1.0 304 Not Modified");
        $this->setCode(304);
        $this->flush();
    }

    /**
     * Send HTTP response resource not found
     *
     * @void
     */
    public function sendNotFound() {
        $this->setHeader("HTTP/1.0 404 Not Found");
        $this->setCode(404);
        $this->flush();
    }

    /**
     * Send HTTP binary response
     *
     * @void
     */
    public function send($cache, $content, $contentType = "application/octet-stream", $gz = false) {
        $this->setCache($cache);
        $this->setHeader(sprintf("Content-Type: %s", $contentType));
        $this->setHeader(sprintf("Content-Length: %d", strlen($content)));
        if ($gz) {
            $this->setHeader("Content-Transfer-Encoding: binary");
            $this->setHeader("Content-Encoding: gzip");
            $this->setHeader("Vary: Accept-Encoding");
        }
        $this->setContent($content);
        $this->flush();
    }
}

/**
 * Implementasi service-based framework. Idenya adalah menjalankan setiap
 * script sebagai komponen independen sebuah service (konsep OSGi)
 * sehingga pengembangan setiap script bisa dilakukan terpisah.
 *
 * Pendekatan ini dilakukan untuk memudahkan delegasi pengembangan sebuah
 * fungsi pada aplikasi.
 *
 * @singleton
 * @private
 */
final class Framework {
    /* service info collections */
    private $anno = null, $processors = array(), $classes = array();

    /* init properties */
    private $className, $requestMethod, $arguments = null, $init = true;

    /* request parameter (json is true for XMLHttpRequest) */
    private $params, $json = true;

    /* registered singleton classes */
    static private $cache = array();

    /**
     * register instance as singleton for further lookup
     */
    static public function register($obj) {
        $name = get_class($obj);
        if (isset(self::$cache[$name])) quit(sprintf("SingletonException: %s", $name));
        self::$cache[$name] = $obj;
    }

    /**
     * Pada production environment, resource header akan di cache untuk tujuan
     * performansi.
     */
    private function configure($className, $arguments = null) {
        $r = new ReflectionClass($className);
        $f = false;
        $c = null;
        if (CACHE) $c = apc_fetch($className, $f);
        if ($f == false) {
            $i = $j = array();
            $m = $r->getMethods();
            foreach($m as $n) {
                if (!$n->isConstructor() && !$n->isDestructor() && !$n->isStatic() &&
                    !$n->isAbstract() && $n->isPublic())
                {
                    $i[$n->getName()] = $this->parse($n->getDocComment());
                }
            }
            $m = $r->getProperties();
            foreach($m as $n) {
                if ($n->isPublic()) {
                    $j[$n->getName()] = $this->parse($n->getDocComment());
                }
            }
            $c = (object) array(
                "C" => $this->parse($r->getDocComment()), "F" => $i, "M" => $j
            );
            if (CACHE) apc_store($className, $c);
        }

        return ($this->classes[$className] = array(
            (sizeof($arguments)) ? $r->newInstanceArgs($arguments) : $r->newInstance(), $c
        ));
    }

    private function parse($comment) {
        $text = meta();
        $anno = array();
        if (preg_match_all($text->regex[2], $comment, $match)) {
            array_pop($match[1]);
            $last = -1;
            $name = null;
            $mark = false;
            foreach($match[1] as $line) {
                $line = trim($line);
                if (strpos($line, "@") === 0) {
                    if (!$mark) $mark = true;
                    $strs = preg_split($text->regex[3], substr($line, 1));
                    $name = array_shift($strs);
                    $anno[$name][++$last] = implode(" ", $strs);
                } else {
                    if ($mark) $anno[$name][$last].= sprintf(" %s", $line);
                }
            }
        }
        $temp = array();
        foreach($anno as $name => $json) {
            $className = null;
            if (!isset($this->processors[$className])) {
                if (!$this->anno->contains($name)) continue;   /* doclet?! */
                $className = $this->anno->$name;
                $this->processors[$className] = new $className;
            }
            $processor = $this->processors[$className];
            if ($processor instanceof AnnotationProcessor) {
                $temp = array_merge($temp, array($className => $processor->parse($json, $comment)));
            } else {
                quit($text->processorException($className));
            }
        }

        return $temp;
    }

    /**
     * Dispatch URL into resource
     */
    public function dispatch($u, $v = null) {
        $text = meta();
        $instance = $this->getService($u);
        $anno = $this->getAnnotation($u);
        if (!isset($anno->F[$this->requestMethod])) {
            if (!is_null($v)) $this->requestMethod = array_shift($v);
            if ($this->init) {
                $this->init = false;
                $this->arguments = $v;
            }
        }
        if (!isset($anno->F[$this->requestMethod]))
        {
            quit($text->methodException($this->className, $this->requestMethod));
        }
        foreach($anno->C as $p => $json) $this->getProcessor($p)->trace($json, $instance);
        foreach($anno->M as $q) {
           foreach($q as $p => $json) $this->getProcessor($p)->trace($json, $instance);
        }
        $methods = $anno->F[$this->requestMethod];
        foreach($methods as $method => $json) {
            $this->getProcessor($method)->trace($json, $instance);
        }
        $callback = array($instance, $this->requestMethod);
        $feedback = (!is_null($v) && ($u === $this->className)) ? call_user_func_array($callback, $v) : call_user_func($callback);

        return $feedback;
    }

    public function __destruct() {
        $this->anno = null;
        $this->processors = null;
        $this->classes = null;
        self::$cache = null;
    }
    public function __construct() {
        self::register($this);
        $text = meta();
        $this->anno = $text->Annotation;
        if ((isset($_SERVER[$text->env->type])) && (stripos($_SERVER[$text->env->type], $text->env->json) !== false))
        {
            $json = json_decode(file_get_contents($text->env->rreq));
            foreach($_GET as $k => $v) $json->$k = $v;
            $this->params = $json;
        } else {
            $this->params = array_merge_recursive($_POST, $_GET);
            $this->json = false;
        }
    }

    /**
     * Get request parameter
     */
    public function getParameter($name) {
        return (is_set($this->params, $name)) ? (($this->json) ? $this->params->$name : $this->params[$name]) : null;
    }

    /**
     * Occasionally we need to set parameter to be available for any services depends on it.
     */
    public function setParameter($name, $value) {
        if ($this->json) $this->params->$name = $value; else $this->params[$name] = $value;
    }

    /**
     * Check if this request is modified since given timestamp
     */
    public function ifModifiedSince($ts) {
        $modified = true;
        if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
            if (strtotime($_SERVER["HTTP_IF_MODIFIED_SINCE"]) >= $ts) $modified = false;
        }

        return $modified;
    }

    /**
     * Get method arguments
     */
    public function arguments() {return $this->arguments;}

    /**
     * Check if particular request is made by Ajax.
     */
    public function isAjax() {
        static $ajax;
        if (is_null($ajax)) {
            $ajax = is_set($_SERVER, meta()->env->hxfw, meta()->env->xreq, true);
        }

        return $ajax;
    }

    /**
     * Check if current request from registered user
     */
    public function isUser() {
        return (isset($_SESSION[meta()->Session->id]));
    }

    /**
     * Get (if logged) current user
     */
    public function getUser() {
        return @$_SESSION[meta()->Session->id];
    }

    /**
     * Check if browser send expected cookies.
     */
    public function supportCookie() {
        static $sc;
        if (is_null($sc)) $sc = (sizeof($_COOKIE) > 0);

        return $sc;
    }

    /**
     * Parse query string into it namespace and arguments
     */
    public function parseQuery($uri) {
        $text = meta();
        $ask = strpos($uri, $text->regex[0]);
        if ($ask !== false) $uri = substr($uri, 0, $ask);
        $def = $text->env->home;
        if (substr($uri, -1) === FILE_SEPARATOR) {
            $uri.= QUERY_CHAR;
            $uri.= $def;
        }
        $arg = null;
        $uri = preg_replace($text->regex[1], FILE_SEPARATOR, trim($uri, FILE_SEPARATOR));
        $ask = strpos($uri, QUERY_CHAR);
        if ($ask === false) {
            if ($uri != STR_EMPTY) $arg = explode(FILE_SEPARATOR, $uri);
        } else {
            if ($ask >= 0) {
                $def = substr($uri, 0, $ask);
                $sub = substr($uri, $ask + 1);
                $ask = strpos($sub, FILE_SEPARATOR);
                $tmp = $sub;
                if ($ask !== false) {
                    $tmp = substr($sub, 0, $ask);
                    $arg = explode(FILE_SEPARATOR, substr($sub, $ask + 1));
                }
                $def.= $tmp;
            }
        }

        return array($def, $arg);
    }

    /**
     * Get annotation class for specified namespace
     */
    public function getAnnotation($className) {
        return $this->classes[$this->normalize($className)][1];
    }

    /**
     * Get processors for specified namespace
     */
    public function getProcessor($className) {
        return (isset($this->processors[$className])) ? $this->processors[$className] : ($this->processors[$className] = new $className);
    }

    /**
     * Normalize given class into namespace or path
     */
    public function normalize($className, $namespace = false) {
        return strtr($className, ($namespace) ? array("_" => ".", FILE_SEPARATOR => ".") : array("." => "_", FILE_SEPARATOR => "_"));
    }

    /**
     * Get service instance for optional given (first) arguments
     *
     * @return  registered or new service instance
     */
    public function getService() {
        $args = func_get_args();
        $name = array_shift($args);
        $className = $this->normalize($name);
        $indexName = $className;
        if (sizeof($args) > 0) {
            $indexName.= implode(FILE_SEPARATOR, $args);
        }
        if (isset(self::$cache[$indexName])) return self::$cache[$indexName];
        if (!class_exists($className, false)) {
            require_once(sprintf("%s.php", $name));
            if (!class_exists($className, false)) quit(meta()->resourceNotFound($className));
        }
        $anno = $this->configure($className, $args)[1]->C;
        if (!(is_set($anno, $this->anno->resource, true) ||
              is_set($anno, $this->anno->secure, true) || is_set($anno, $this->anno->private, true) ||
              is_set($anno, $this->anno->protected, true)))
        {
            quit(meta()->resourceNotFound($name));
        }
        $instance = $this->classes[$className][0];
        $instanceName = get_class($instance);
        if ($className !== $instanceName) {
            quit(meta()->resourceNameException($className, $instanceName));
        }
        if (isset($anno[$this->anno->singleton])) self::$cache[$indexName] = $instance;

        return $instance;
    }

    /**
     * Main Entry Point
     *
     * @void
     */
    public function main() {
        if ($this->init === false) return;
        $this->requestMethod = strtoupper($_SERVER[meta()->env->mreq]);
        list($this->className, $this->arguments) = $this->parseQuery((isset($_SERVER[meta()->env->path])) ? $_SERVER[meta()->env->path] : FILE_SEPARATOR);
        $c = $this->getService(meta()->API->Session);
        $x = $this->getService($this->className);
        $z = $this->getAnnotation($this->className);
        if (is_set($z->C, $this->anno->private, true) ||
            is_set($z->C, $this->anno->protected, true)) quit(meta()->resourceProtected($this->className));
        if (is_set($z->C, $this->anno->secure, true) && !$this->isUser())
        {
            $this->getService(meta()->API->Response)->redirect(FILE_SEPARATOR);
        }
        $o = null;
        try {
            $o = $this->dispatch($this->className, $this->arguments);
        } catch(Exception $e) {
            $p = ($this->isAjax()) ? "ajax" : "http";
            $p.= "Text";
            $o = meta()->$p($e->getFile(), $e->getLine(), $e->getMessage(), $e->getTraceAsString());
        }
        if (is_set($z->F, $this->anno->silent, true) === false) {
            $j = false;
            $r = false;
            if (is_null($o)) {
                $f = $this->getService(meta()->API->Output)->flush();
                if (!is_null($f)) $o = $f;
            } else {
                if (is_scalar($o)) {
                    if (is_bool($o)) $o = ($o) ? "true" : "false"; else $r = true;
                } else {
                    $isa = is_array($o);
                    if ($isa || is_object($o)) {
                        $o = ($isa) ? json_encode($o) : $o->toString();
                    }
                }
                if ($this->requestMethod === "POST" && is_set($this->params, "CRUD")) $o = meta()->callback($o);
                else {
                    if (!$r) $j = true;
                }
            }
            $r = $this->getService(meta()->API->Response);
            if (!headers_sent()) {
                $z = (is_null($o)) ? false : is_set($z->F[$this->requestMethod], $this->anno->gzip, true);
                $i = null;
                if ($z) {
                    $o = gzencode($o, 9);
                    $i = meta()->headers;
                    $i[0] = sprintf($i[0], strlen($o));
                }
                $r->setContent($o);
                if ($j && $this->isAjax()) $i[] = meta()->jsonType;
                if (!is_null($i)) {
                    foreach($i as $j) $r->setHeader($j);
                }
            }
            $r->flush();
        }
    }
}

/**
 * fetch resource berdasarkan namespace.
 *
 * @return  service instance
 */
function getService(/* String */ $namespace = null) {
    static $ufwk = new Framework;

    return (is_null($namespace)) ? $ufwk : $ufwk->getService($namespace);
}

/**
 * @return  cached array/object
 */
function meta() {
    static $meta = new MetaHelper;
    return $meta;
}

/**
 * @return  Connection instance
 */
function getConnection($jndi = "ufwk") {
    static $jdbc = array();
    if (isset($jdbc[$jndi])) return $jdbc[$jndi];
    try {
        $jdbc[$jndi] = new Connection($jndi);
    } catch(Exception $e) {
        quit($e->getMessage());
    }

    return $jdbc[$jndi];
}

/**
 * Strictly-check array-key atau object-property. Naturally, PHP adalah
 * loosely type language, untungnya, operator "aneh" seperti === bisa
 * digunakan untuk masalah ini.
 *
 * @param   $map    array or object
 * @param   $key    String
 * @param   $def    optional matched String
 * @param   $strict boolean
 *
 * @return  boolean
 */
function is_set($map, $key, $def = null, $strict = false) {
    $a = is_array($map);
    $v = ($a) ? isset($map[$key]) : property_exists($map, $key);
    if ($v && !is_null($def)) {
        $m = ($a) ? $map[$key] : $map->$key;
        $v = ($strict) ? ($m === $def) : ($m == $def);
    }

    return $v;
}

/**
 * Keluar proses, dengan format argument (jika diperlukan)
 *
 * quit(String message);
 * quit(String message, Object... arguments);
 *
 * @void
 */
function quit() {
    $arguments = func_get_args();
    $format = array_shift($arguments);
    file_put_contents(STDOUT, (sizeof($arguments) > 0) ? vsprintf($format, $arguments) : $format);
    exit(200);
}
?>